【AWS CDK】cdk-nagのテストをGitHub Actionsに追加して、セキュリティチェックを自動化する
cdk-nag は Constructs がルールに準拠しているかを検証してくれるツールで、多くのルールセットがあります。
まだ cdk-nag について知らないという方は、以下を参照してください。
上記の記事では cdk-nag を cdk synth したタイミングで実行し、エラーを確認していました。
これでも良いのですが、できればテストに組み込んでエラーがある場合は修正するフローにしたいですよね。
今回は cdk-nag をアサーションテストに組み込み、CI として実行するところまで確認してみます。
cdk-nagをテストで実行するメリット
cdk-nag をテストに追加し自動化することで、以下のようなメリットがあります。
- テスト自動化による継続的なセキュリティチェック
- 開発段階からセキュアな環境構築が可能
- チーム全体でセキュリティ基準の統一が測れる
導入自体それほどハードルが高くないため、セキュリティに不安がある場合ほど導入するメリットが大きいです。
これらをレビューや個別にテストコードを書いて対応するのは大変なので、テストによる自動化をしていきましょう。
テストコード実装
以下の AWS ブログの中にアサーションテストを用いたユニットテストの実装例があったので、これを参考に導入してみます。
前提
リソースは S3 バケットを作成しただけのものを用意します。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { Bucket } from "aws-cdk-lib/aws-s3";
import { NagSuppressions } from "cdk-nag";
export class CdkNagTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const bucket = new Bucket(this, "Bucket", {});
}
}
テストコードを追加する
AWS ブログにあった内容で実装してみます。
import { Annotations, Match } from "aws-cdk-lib/assertions";
import { App, Aspects, Stack } from "aws-cdk-lib";
import { AwsSolutionsChecks } from "cdk-nag";
import { CdkNagTestStack } from "../lib/cdk-nag-test-stack";
describe("cdk-nag AwsSolutions Pack", () => {
let stack: Stack;
let app: App;
// In this case we can use beforeAll() over beforeEach() since our tests
// do not modify the state of the application
beforeAll(() => {
// GIVEN
app = new App();
stack = new CdkNagTestStack(app, "test");
// WHEN
Aspects.of(stack).add(new AwsSolutionsChecks());
});
// THEN
test("No unsuppressed Warnings", () => {
const warnings = Annotations.fromStack(stack).findWarning(
"*",
Match.stringLikeRegexp("AwsSolutions-.*"),
);
expect(warnings).toHaveLength(0);
});
test("No unsuppressed Errors", () => {
const errors = Annotations.fromStack(stack).findError(
"*",
Match.stringLikeRegexp("AwsSolutions-.*"),
);
expect(errors).toHaveLength(0);
});
});
cdk-nag の実行結果からWarning
とError
それぞれ取得して、0 件であることをテストしています。今回はAwsSolutions
のルールセットを利用しているため、取得対象もAwsSolutions
のものだけになっています。
もしPCI DSS 3.2.1
等別のルールセットの場合は、適宜修正してください。
これでnpm run test
を実行してみます。
テキストでの確認はこちら
❯ npm run test
> cdk-nag-test@0.1.0 test
> jest
FAIL test/cdk-nag-test.test.ts
cdk-nag AwsSolutions Pack
✓ No unsuppressed Warnings (196 ms)
✕ No unsuppressed Errors (12 ms)
● cdk-nag AwsSolutions Pack › No unsuppressed Errors
expect(received).toHaveLength(expected)
Expected length: 0
Received length: 2
Received array: [{"entry": {"data": "AwsSolutions-S1: The S3 Bucket has server access logs disabled.
", "trace": ["Annotations.addMessage (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/annotations.js:1:1710)", "Annotations.addError (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/annotations.js:1:1202)", "AnnotationLogger.onNonCompliance (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-logger.ts:142:37)", "/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-pack.ts:185:17", "Array.forEach (<anonymous>)", "AwsSolutionsChecks.applyRule (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-pack.ts:184:26)", "AwsSolutionsChecks.checkStorage (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/packs/aws-solutions.ts:401:10)", "AwsSolutionsChecks.visit (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/packs/aws-solutions.ts:200:12)", "recurse (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:3976)", "recurse (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:4160)", …], "type": "aws:cdk:error"}, "id": "/test/Bucket/Resource", "level": "error"}, {"entry": {"data": "AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL.
", "trace": ["Annotations.addMessage (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/annotations.js:1:1710)", "Annotations.addError (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/annotations.js:1:1202)", "AnnotationLogger.onNonCompliance (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-logger.ts:142:37)", "/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-pack.ts:185:17", "Array.forEach (<anonymous>)", "AwsSolutionsChecks.applyRule (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/nag-pack.ts:184:26)", "AwsSolutionsChecks.checkStorage (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/packs/aws-solutions.ts:428:10)", "AwsSolutionsChecks.visit (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/cdk-nag/src/packs/aws-solutions.ts:200:12)", "recurse (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:3976)", "recurse (/Users/suzuki.jun/Documents/github_blog/blog/cdk-nag-test/node_modules/aws-cdk-lib/core/lib/private/synthesis.js:2:4160)", …], "type": "aws:cdk:error"}, "id": "/test/Bucket/Resource", "level": "error"}]
32 | Match.stringLikeRegexp("AwsSolutions-.*"),
33 | );
> 34 | expect(errors).toHaveLength(0);
| ^
35 | });
36 | });
37 |
at Object.<anonymous> (test/cdk-nag-test.test.ts:34:20)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.586 s, estimated 4 s
Ran all test suites.
2 件のエラーが検知されたことを確認できました。
少し分かりづらいですが、AwsSolutions-S1
とAwsSolutions-S10
が検出されています。
エラーメッセージの整形
これでも cdk-nag によるエラーが発生していることは分かるのですが、エラーメッセージが長文で分かりにくいです。
整形したいなと思っていたところ、以下の素晴らしいブログがあったのでこちらの実装をお借りしました。非常に分かりやすく解説されているので、ぜひ一度読んでみてください。
一部 linter、formatter による修正がありますが、ほぼ同じ内容です。
import { Annotations, Match } from "aws-cdk-lib/assertions";
import { App, Aspects, Stack } from "aws-cdk-lib";
import { AwsSolutionsChecks } from "cdk-nag";
import { CdkNagTestStack } from "../lib/cdk-nag-test-stack";
import { SynthesisMessage } from "aws-cdk-lib/cx-api/lib/metadata";
describe("cdk-nag AwsSolutions Pack", () => {
let stack: Stack;
let app: App;
beforeEach(() => {
// GIVEN
app = new App();
stack = new CdkNagTestStack(app, "test");
// WHEN
Aspects.of(stack).add(new AwsSolutionsChecks());
});
// THEN
test("No unsuppressed Warnings", () => {
const warnings = Annotations.fromStack(stack).findWarning(
"*",
Match.stringLikeRegexp("AwsSolutions-.*"),
);
try {
expect(warnings).toHaveLength(0);
} catch (e) {
throw new Error(createCdkNagLog(warnings));
}
});
test("No unsuppressed Errors", () => {
const errors = Annotations.fromStack(stack).findError(
"*",
Match.stringLikeRegexp("AwsSolutions-.*"),
);
try {
expect(errors).toHaveLength(0);
} catch (e) {
throw new Error(createCdkNagLog(errors));
}
});
});
function createCdkNagLog(messages: SynthesisMessage[]): string {
let log = "";
for (const message of messages) {
switch (message.level) {
case "info":
log += "\u001b[34m"; // blue
break;
case "warning":
log += "\u001b[33m"; // yellow
break;
case "error":
log += "\u001b[31m"; // red
break;
default:
log += "\u001b[30m"; // black
break;
}
log += `[${message.level} at ${message.id}] ${message.entry.data as string}\u001b[0m`;
}
return log;
}
修正が完了したら、npm run test
を実行してみます。
テキストでの確認はこちら
❯ npm run test
> cdk-nag-test@0.1.0 test
> jest
FAIL test/cdk-nag-test.test.ts
cdk-nag AwsSolutions Pack
✓ No unsuppressed Warnings (289 ms)
✕ No unsuppressed Errors (16 ms)
● cdk-nag AwsSolutions Pack › No unsuppressed Errors
[error at /test/Bucket/Resource] AwsSolutions-S1: The S3 Bucket has server access logs disabled.
[error at /test/Bucket/Resource] AwsSolutions-S10: The S3 Bucket or bucket policy does not require requests to use SSL.
38 | expect(errors).toHaveLength(0);
39 | } catch (e) {
> 40 | throw new Error(createCdkNagLog(errors));
| ^
41 | }
42 | });
43 | });
at Object.<anonymous> (test/cdk-nag-test.test.ts:40:13)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 3.55 s, estimated 4 s
Ran all test suites.
これでエラーが発生している件数と内容が分かりやすくなりました。これであればうまく運用できそうです。
GitHub Actions上で動作させる
ローカルで cdk-nag のテスト確認ができたので、GitHub Actions 上で CI として実行してみます。
.github/workflows/cdk-nag-test.yml
のファイルを作成し、テストが実行されるように記述します。
Node.js をセットアップして、依存関係のインストールとテスト実行するだけのシンプルなものです。
name: CDK Nag Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 20
uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
これで準備ができたので、GitHub 上に Push して動作を確認してみます。
エラーを修正せずに Push したため、同じエラーが発生しました。
エラーを修正するため、lib/cdk-nag-test-stack.ts
を以下のように修正します。
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import { Bucket } from "aws-cdk-lib/aws-s3";
+import { NagSuppressions } from "cdk-nag";
export class CdkNagTestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
+ NagSuppressions.addStackSuppressions(this, [
+ {
+ id: "AwsSolutions-S1",
+ reason: "This is a demo bucket, so it does not need access logs",
+ },
]);
const bucket = new Bucket(this, "Bucket", {
+ enforceSSL: true,
});
}
}
AwsSolutions-S1
は抑制し、AwsSolutions-S10
は修正しました。
この状態でローカルテストを実行すると、成功することが確認できます。
❯ npm run test
> cdk-nag-test@0.1.0 test
> jest
PASS test/cdk-nag-test.test.ts
cdk-nag AwsSolutions Pack
✓ No unsuppressed Warnings (231 ms)
✓ No unsuppressed Errors (19 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 2.136 s, estimated 4 s
Ran all test suites.
問題なさそうなので、GitHub にもう一度 Push して成功するか確認します。
無事テストが成功したことを確認できました。
まとめ
cdk-nag を使ったテスト導入と GitHub Actions 上での動作を確認してみました。npm test で実行できるため、CI パイプラインに組み込みやすい点も導入しやすいですね。
CDK では L2 コンストラクトを使っている場合、自動で設定される値がセキュアではないケースが多々あります。cdk-nag をユニットテストで実行することで、セキュアな状態で開発ができるので、ぜひ導入を検討してみてください。
導入コストは小さいですが、既存プロジェクトの場合は多くのエラーが出る可能性もあります。段階的に対応していき、抑制する場合は明確な理由を記載するなど運用ルールを決めていきましょう。
CDK を利用している方は、ぜひ本記事を参考にプロジェクトのセキュリティ強化に取り組んでみてください。